feat: cluster-scoped RBAC visibility (denied-state UX)#988
Merged
Conversation
1e0e55f to
c220992
Compare
When a user's RBAC denies listing a core cluster-scoped kind (Node, PV, StorageClass, Namespace, ...), resource-counts silently omitted it, so the UI rendered '0 / No Node found' — indistinguishable from an empty cluster. Surface these in the existing forbidden set so the frontend's existing 'Access Restricted' state renders instead. CRDs stay omitted (could be not-installed vs denied).
The forbidden empty state was a bare 'Access Restricted / Insufficient permissions'. Replace with a reusable RestrictedState component: plain-language framing for a non-k8s operator (states it's RBAC, not an empty cluster), and a 'How to get access' disclosure with a generic ClusterRole/ClusterRoleBinding snippet + 'Copy request' button to forward to whoever administers the cluster. Honest about cause (never claims tier vs custom-role) and chart-version-agnostic (generic YAML).
Bugbot triage on the forbidden-state component: - Expanded RBAC snippet could clip on short panes (absolute inset-0 centered, no scroll). Wrap in overflow-y-auto + min-h-full so it centers when collapsed and scrolls (no top clip) when expanded; cap the snippet at max-h-72. - selectedKind.name is the API plural in normal use but a CRD deep-link can transiently carry the Kind (HTTPRoute) before discovery resolves it, yielding a snippet that wouldn't grant access. Only inline the resource when it looks like a valid lowercase resource name; otherwise use a <resource> placeholder and a generic role name.
Reframe the My Permissions dialog as 'Your access on this cluster' and add a 'Restricted resources' section: the kinds your RBAC can't list (from the resource-counts forbidden set) shown as chips, with honest framing (an access boundary, not an empty cluster) and a 'How to get access' link to the RBAC docs. This is the comprehension anchor for the denied-vs-empty work — what you CAN do (SelfSubjectRulesReview rules) alongside what's hidden and how to unblock.
…list 403 The main-panel restricted state keyed only off the list query returning 403, but handleListResources returns 200 with [] for denied cluster-scoped kinds — so a denied Node/PV fell through to 'No <kind> found' instead of 'Access Restricted', the exact symptom this change set out to fix. Also trigger isSelectedForbidden when the selected kind is in the counts forbidden set (which the backend now populates for denied cluster-scoped kinds). Bugbot caught this.
resource-counts now tags each forbidden kind with a reason: 'rbac_denied' (Radar's ServiceAccount can read the kind but the user's own RBAC can't — a user grant surfaces it) vs 'unavailable' (no informer at all: not installed, SA lacks RBAC, or feature off like rbac.viewRBAC — a user grant won't help). RestrictedState branches on it: rbac_denied keeps the 'grant your identity' request + ClusterRole snippet; unavailable explains that Radar's ServiceAccount can't read the kind and omits the (useless) user-grant snippet. Fixes the overclaim where feature-disabled kinds (e.g. viewRBAC-off ClusterRoles) were told 'your RBAC doesn't allow' with a snippet that wouldn't surface them.
0d220c9 to
fc7185c
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fc7185c. Configure here.
… pluralized kind The grant snippet dropped the natural-language preamble (just the ClusterRole/ClusterRoleBinding YAML now), the Copy button moved into a header bar so it no longer overlaps the first code line, and the denied copy reads "<Kind> resources" to avoid awkward singular phrasing.
Guards the counts-driven forbidden gate against a startup timing skew where a kind is marked forbidden/unavailable in resource-counts before its informer syncs, while the list query has since returned data. Actual rows now win; a list 403 still forces the restricted state.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What
RBAC-restricted users (notably Cloud
viewer/member/ownertiers) get an honest, actionable view of cluster-scoped resources they cannot list — instead of a kind silently rendering "0 / No X found", which looks like an empty cluster.Changes
resource_counts.go): when RBAC denies a core cluster-scoped kind (Node, PV, StorageClass, …), report it inforbiddenwith areasonsmap distinguishingrbac_denied(your identity can't list it — a grant helps) fromunavailable(Radar's own ServiceAccount can't read it — a user grant won't help).RestrictedState(@skyhook-io/k8s-ui): a reusable "you don't have access to X" state that branches on the reason —rbac_deniedshows a copyableClusterRole/ClusterRoleBindingrequest to hand to a cluster admin;unavailableexplains the platform itself lacks the read and omits the snippet. Replaces the bare "Access Restricted" line in the resource list. Fires on the countsforbiddenset too, since cluster-scoped denials return200 + []rather than403.SelfSubjectRulesReviewrules, with a docs link to the unblock RBAC.Tests
Unit:
TestResourceCountsSurfacesDeniedCoreClusterScopedKindAsForbidden;make tsc+go test ./internal/servergreen.Visual — local
kindcluster, two Radar instances staging the reason codes against a real apiserver (proxy-mode admin SA + a restricted impersonated user →rbac_denied; no-auth as a restricted ServiceAccount →unavailable):rbac_deniedlist (/resources/nodes, node-denied user)SelfSubjectRulesReviewgrants + "Restricted or unavailable (6)" section (Node, PV, IngressClass, StorageClass, ClusterRole, ClusterRoleBinding) + docs linkunavailablelist (Radar SA can't read the kind)rbac_denied)Related
authentication.mdaccuracy fix shipped separately in docs: correct stale cluster-scoped RBAC gating description #973 (merged).Note
Medium Risk
Changes RBAC-aware resource-counts and list empty-state behavior for cluster-scoped kinds; mistakes could mislabel access or leak counts, but scope is bounded and covered by new server tests.
Overview
RBAC-restricted users no longer see "0 / No X found" for cluster-scoped kinds they cannot list (e.g. Nodes).
/api/resource-countsnow adds areasonsmap (rbac_deniedvsunavailable) and puts denied core cluster-scoped kinds inforbiddenwhen the user fails SAR but Radar’s SA can still read them.The resource list uses a shared
RestrictedState:rbac_deniedshows copyable ClusterRole/ClusterRoleBinding YAML;unavailableexplains Radar cannot read the kind and omits the snippet. Detection also uses countsforbidden, not only list 403, since cluster-scoped denials can return 200 + []. Actual rows override a stale forbidden entry.Your access on this cluster (My Permissions) lists hidden kinds from
forbiddenwith docs link.TestResourceCountsSurfacesDeniedCoreClusterScopedKindAsForbiddencovers the Node case.Reviewed by Cursor Bugbot for commit 98f090a. Bugbot is set up for automated code reviews on this repo. Configure here.